﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Net;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Security.Permissions;
using System.Threading;
using System.Xml;
using InfiniTec.Diagnostics;
using InfiniTec.Runtime.Serialization;
using InfiniTec.Threading;

namespace InfiniTec.Exchange.Notifications
{
    /// <summary>
    /// Maintains a collection of <see cref="Subscription"/> instances.
    /// </summary>
    /// <remarks>
    /// <para>
    /// The collection of subscriptions can be serialized to a <see cref="Stream"/> or <see cref="XmlWriter"/> by calling either
    /// <see cref="Serialize(Stream)"/> or <see cref="Serialize(XmlWriter)"/>.
    /// </para>
    /// <para>
    /// To deserialize a collection, call the <see cref="Deserialize(System.IO.Stream,InfiniTec.Exchange.Notifications.IExchangeWebServicesAdapter,InfiniTec.Exchange.Notifications.PushNotificationListener)"/> 
    /// or <see cref="Deserialize(System.Xml.XmlReader,InfiniTec.Exchange.Notifications.IExchangeWebServicesAdapter,InfiniTec.Exchange.Notifications.PushNotificationListener)"/> method. To automatically
    /// restart all the subscriptions which where active at the time of serialization, call the <see cref="RestartAsync"/> method. It will reactivate
    /// all formerly active subscriptions on a background thread and raise the <see cref="SubscriptionRestartProgressChanged"/> and 
    /// <see cref="SubscriptionRestartCompleted"/> event during the process.
    /// </para>
    /// </remarks>
    [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")]
    [Serializable]
    [KnownType(typeof (List<Subscription>))]
    [KnownType(typeof (ImpersonationSettings))]
    public sealed class SubscriptionCollection : IEnumerable<Subscription>, ISerializable
    {
        private PushNotificationListener _Listener;
        internal static readonly Trace Trace = new Trace("InfiniTec.Exchange.Notifications");

        private readonly List<Subscription> _Subscriptions = new List<Subscription>();
        [NonSerialized] private IExchangeWebServicesAdapter _WebServiceAdapter;

    	/// <summary>
    	/// Initializes a new instance of the <see cref="SubscriptionCollection"/> class.
    	/// </summary>
    	/// <param name="webServiceAdapter">The web service adapter used to manage subscriptions.</param>
    	public SubscriptionCollection(IExchangeWebServicesAdapter webServiceAdapter) 
    	{
			if (webServiceAdapter == null) throw new ArgumentNullException("webServiceAdapter");
			WebServiceAdapter = webServiceAdapter;
    	}

    	/// <summary>
		/// Initializes a new instance of the <see cref="SubscriptionCollection"/> class.
		/// </summary>
		/// <param name="webServiceAdapter">The web service adapter used to manage subscriptions.</param>
		/// <param name="listener">The listener to associate this collection with.</param>
        public SubscriptionCollection(IExchangeWebServicesAdapter webServiceAdapter, PushNotificationListener listener): this(webServiceAdapter)
        {
			if (listener == null) throw new ArgumentNullException("listener");
			_Listener = listener;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="SubscriptionCollection"/> class.
        /// </summary>
        public SubscriptionCollection()
        {
        }

        private SubscriptionCollection(SerializationInfo info, StreamingContext context)
        {
            _Subscriptions = info.GetValue<List<Subscription>>("Subscriptions");
        }

        /// <summary>
        /// Gets or sets the web service adapter used to manage subscriptions on an Exchange 2007 CAS role.
        /// </summary>
        /// <value>The web service adapter.</value>
        private IExchangeWebServicesAdapter WebServiceAdapter
        {
            get { return _WebServiceAdapter; }
            set { _WebServiceAdapter = value; }
        }

        /// <summary>
        /// Gets the number of elements actually contained in the <see cref="T:System.Collections.Generic.List`1"/>.
        /// </summary>
        /// <returns>
        /// The number of elements actually contained in the <see cref="T:System.Collections.Generic.List`1"/>.
        /// </returns>
        public int Count
        {
            get { return _Subscriptions.Count; }
        }

        public IEnumerator<Subscription> GetEnumerator()
        {
            return _Subscriptions.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

		[SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)]
        void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
        {
            info.AddValue("Subscriptions", _Subscriptions);
        }

        /// <summary>
        /// Raised for each subscription restarted on the background thread.
        /// </summary>
        [field: NonSerialized]
        public event EventHandler<ProgressStatusChangedEventArgs> SubscriptionRestartProgressChanged;

        /// <summary>
        /// Occurs when all subscriptions have been restarted. The event arguments contain a list of errors for each failed subscription request.
        /// </summary>
        [field: NonSerialized]
        public event AsyncCompletedEventHandler<SubscriptionRestartErrorSummary> SubscriptionRestartCompleted;


        /// <summary>
        /// Creates a new subscription for the specified folders and events.
        /// </summary>
        /// <param name="folders">A list of references to folders to monitor for changes.</param>
        /// <param name="events">A valid combination of the <see cref="EventTypes"/> values, except for <see cref="EventTypes.None"/>.</param>
        /// <returns>The newly created <see cref="Subscription"/> instance.</returns>
        public Subscription Add(IEnumerable<FolderReference> folders, EventTypes events, SubscriptionType subscriptionType)
        {
            if (WebServiceAdapter == null)
            {
                throw new InvalidOperationException("The WebServiceAdapter property has to be set before subscriptions can be created.");
            }

			if (subscriptionType != SubscriptionType.Pull && subscriptionType != SubscriptionType.Push)
				throw new InvalidEnumArgumentException("subscriptionType", (int)subscriptionType, typeof(SubscriptionType));

            var subscription = new Subscription(folders, events, WebServiceAdapter, subscriptionType, _Listener);
            _Subscriptions.Add(subscription);
            return subscription;
        }

        /// <summary>
        /// Determines whether an element is in the <see cref="T:System.Collections.Generic.List`1"/>.
        /// </summary>
        /// <returns>
        /// true if <paramref name="item"/> is found in the <see cref="T:System.Collections.Generic.List`1"/>; otherwise, false.
        /// </returns>
        /// <param name="item">The object to locate in the <see cref="T:System.Collections.Generic.List`1"/>. The value can be null for reference types.
        ///                 </param>
        public bool Contains(Subscription item)
        {
            return _Subscriptions.Contains(item);
        }


		/// <summary>
		/// Removes the specified subscription from the current <see cref="SubscriptionCollection"/> instance.
		/// The subscription itself will be stopped and no further events will be raised.
		/// </summary>
		/// <param name="item">The item.</param>
		/// <returns></returns>
        public bool Remove(Subscription item)
        {
            if (item == null) throw new ArgumentNullException("item");
            if (_Subscriptions.Remove(item))
            {
                _Listener.Unregister(item);
                item.Stop();
                return true;
            }
            return false;
        }

        /// <summary>
        /// Searches for the specified object and returns the zero-based index of the first occurrence within the entire <see cref="T:System.Collections.Generic.List`1"/>.
        /// </summary>
        /// <returns>
        /// The zero-based index of the first occurrence of <paramref name="item"/> within the entire <see cref="T:System.Collections.Generic.List`1"/>, if found; otherwise, –1.
        /// </returns>
        /// <param name="item">The object to locate in the <see cref="T:System.Collections.Generic.List`1"/>. The value can be null for reference types.
        ///                 </param>
        public int IndexOf(Subscription item)
        {
            return _Subscriptions.IndexOf(item);
        }

        /// <summary>
        /// Restarts subscriptions which where active before the current collection instance was serialized.
        /// </summary>
        /// <remarks>
        /// Only call this method once after deserialization. Be sure to attach event handlers to the subscriptions before calling this method,
        /// as the Exchange server will send out missed notification instantly.
        /// </remarks>
        public void RestartAsync()
        {
            var subscriptions = new Queue<Subscription>(this.Where(subscription => subscription.IsRestartNeeded));

            int processes = Math.Min(ServicePointManager.DefaultConnectionLimit, subscriptions.Count);
            var status = new SubscriptionRestartStatus(processes, subscriptions.Count);

            for (int i = 0; i < processes; i++)
            {
                ThreadPool.QueueUserWorkItem(state => RestartActiveSubscriptionsInternal(subscriptions, status), null);
            }
        }

		[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
		private void RestartActiveSubscriptionsInternal(Queue<Subscription> subscriptions, SubscriptionRestartStatus status)
        {
            do
            {
                Subscription subscription = null;
                try
                {
                    lock (subscriptions)
                    {
                        if (subscriptions.Count == 0)
                        {
                            break;
                        }
                        subscription = subscriptions.Dequeue();
                    }
                    subscription.Start();
                }
                catch (Exception ex)
                {
                    status.RegisterFailedSubscription(subscription, ex);
                    Trace.Error("Failed to start subscription. Error: {0}", ex);
                }
                int progressPercentage = status.CompleteSubscription();
                SubscriptionRestartProgressChanged.Raise(this, new ProgressStatusChangedEventArgs(null, progressPercentage, null));
            } while (true);

            if (status.CompleteRestartProcessor())
            {
                SubscriptionRestartCompleted.Raise(this, new AsyncCompletedEventArgs<SubscriptionRestartErrorSummary>(new SubscriptionRestartErrorSummary(status.Errors), null));
            }
        }

        /// <summary>
        /// Serializes the current instance to the specified output stream using a <see cref="BinaryFormatter"/>.
        /// </summary>
        /// <param name="outputStream">The output stream to write to..</param>
        public void Serialize(Stream outputStream)
        {
            if (outputStream == null) throw new ArgumentNullException("outputStream");
            if (!outputStream.CanWrite) throw new ArgumentException("The outputStream must be writable.", "outputStream");
            var binaryFormatter = new BinaryFormatter();

            binaryFormatter.Serialize(outputStream, this);
        }

        /// <summary>
        /// Serializes the current instance to the specified <see cref="XmlWriter"/> using a <see cref="DataContractSerializer"/>.
        /// </summary>
        /// <param name="writer">The writer to use for serialization.</param>
        /// <remarks>
        /// This method requires the .NET Framework 3.5 with Service Pack 1. 
        /// </remarks>
        public void Serialize(XmlWriter writer)
        {
            if (writer == null) throw new ArgumentNullException("writer");
            var serializer = new DataContractSerializer(typeof (SubscriptionCollection));

            serializer.WriteObject(writer, this);
        }

        /// <summary>
        /// Deserializes a <see cref="SubscriptionCollection"/> from the specified <see cref="XmlReader"/>.
        /// </summary>
        /// <param name="reader">The reader to deserialize the <see cref="SubscriptionCollection"/> from.</param>
        /// <param name="webServicesAdapter">An implementation of an <see cref="IExchangeWebServicesAdapter"/> that manages subscriptions on the Exchange server.</param>
        /// <returns>A <see cref="SubscriptionCollection"/> instance.</returns>
        /// <remarks>
        /// This method requires the .NET Framework 3.5 with Service Pack 1.
        /// </remarks>
        public static SubscriptionCollection Deserialize(XmlReader reader, IExchangeWebServicesAdapter webServicesAdapter, PushNotificationListener listener)
        {
            if (reader == null) throw new ArgumentNullException("reader");
            if (webServicesAdapter == null) throw new ArgumentNullException("webServicesAdapter");
            if (listener == null) throw new ArgumentNullException("listener");

            var serializer = new DataContractSerializer(typeof (SubscriptionCollection));
            var subscriptionCollection = (SubscriptionCollection) serializer.ReadObject(reader);
            subscriptionCollection.RegisterWebServiceAdapterAndListener(webServicesAdapter, listener);

            return subscriptionCollection;
        }

        /// <summary>
        /// Deserializes a <see cref="SubscriptionCollection"/> from the specified <see cref="Stream"/>.
        /// </summary>
        /// <param name="inputStream">The input stream to read the collection from.</param>
        /// <param name="webServicesAdapter">An implementation of an <see cref="IExchangeWebServicesAdapter"/> that manages subscriptions on the Exchange server.</param>
        /// <returns>A <see cref="SubscriptionCollection"/> instance.</returns>
        [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)]
        public static SubscriptionCollection Deserialize(Stream inputStream, IExchangeWebServicesAdapter webServicesAdapter, PushNotificationListener listener)
        {
            if (inputStream == null) throw new ArgumentNullException("inputStream");
            if (webServicesAdapter == null) throw new ArgumentNullException("webServicesAdapter");
            if (listener == null) throw new ArgumentNullException("listener");
            if (!inputStream.CanRead) throw new ArgumentException("The input stream must be readable.", "inputStream");

            var binaryFormatter = new BinaryFormatter();
            var subscriptionCollection = (SubscriptionCollection) binaryFormatter.Deserialize(inputStream);
            subscriptionCollection.RegisterWebServiceAdapterAndListener(webServicesAdapter, listener);

            return subscriptionCollection;
        }

        private void RegisterWebServiceAdapterAndListener(IExchangeWebServicesAdapter webServicesAdapter, PushNotificationListener listener)
        {
            if (webServicesAdapter == null) throw new ArgumentNullException("webServicesAdapter");
            if (listener == null) throw new ArgumentNullException("listener");
            _WebServiceAdapter = webServicesAdapter;
            _Listener = listener;

            this.Foreach(subscription =>  
                {
                    subscription.ExchangeWebServicesAdapter = webServicesAdapter;
                    subscription.SetListener(listener);
                });
        }
    }
}